////////////////////////////////////////////////////////////
// Headers
////////////////////////////////////////////////////////////
#include "LightSystem.hpp"

#include <assert.h>

#include <SFML/OpenGL.hpp>

#include <SmegTools/OpenGL.hpp>
#include <SmegTools/Tools.hpp>
#include <SmegTools/Debug/ProfilerManager.hpp>
#include <SmegCore/Math/BoundingVolumes/AABB.hpp>

#include "Light.hpp"
#include "../Hull/Hull.hpp"

#include <SmegTools/Debug/DebugNew.hpp>

////////////////////////////////////////////////////////////
// Defines
////////////////////////////////////////////////////////////
#define TEXTURE_SIZE        1024
#define HALF_TEXTURE_SIZE   512

namespace smeg {
    namespace light {

////////////////////////////////////////////////////////////
/// Default Constructor
////////////////////////////////////////////////////////////
LightSystem::LightSystem() :
    m_AmbientColor( sf::Color::Black ),
    m_Lights(),
    m_LightsTree(),
    m_Hulls(),
    m_HullsTree(),
    m_fAlphaThreshold( 0.95f ),
    m_fBlurRadius( 5.0f )
{
}
    
////////////////////////////////////////////////////////////
/// Destructor
////////////////////////////////////////////////////////////
LightSystem::~LightSystem()
{
    ClearScene();
}

////////////////////////////////////////////////////////////
/// Initialize the LightSystem
////////////////////////////////////////////////////////////
bool LightSystem::Init( const math::AABB& _Region, sf::RenderWindow* _pRenderWindow ) {
    // Setup the trees
    m_LightsTree.Create( _Region );
    m_HullsTree.Create( _Region );

    // Setup the render window
    m_pRenderWindow = _pRenderWindow;
    SetView( m_pRenderWindow->getView() );

    // Light texture
    m_LightTexture.create( TEXTURE_SIZE, TEXTURE_SIZE );
    m_LightTexture.setActive();
    glEnable( GL_BLEND );
    glEnable( GL_TEXTURE_2D );

    if ( !m_ShadowMapShader.loadFromFile( "data/shaders/computeShadowMap.frag", sf::Shader::Fragment ) ) {
        return false;
    }
    m_ShadowMapTex.create(TEXTURE_SIZE,1);
    //m_ShadowMapTex.create(TEXTURE_SIZE,TEXTURE_SIZE);

    if ( !m_DrawShadowsShader.loadFromFile( "data/shaders/drawShadows.frag", sf::Shader::Fragment ) ) {
        return false;
    }
    m_DrawShadowsTex.create(TEXTURE_SIZE,TEXTURE_SIZE);

    if ( !m_BlendStaticDynamicShader.loadFromFile( "data/shaders/blendStaticDynamic.frag", sf::Shader::Fragment ) ) {
        return false;
    }

    if ( !m_BlurShader.loadFromFile( "data/shaders/blur.frag", sf::Shader::Fragment ) ) {
        return false;
    }

    m_pRenderWindow->setActive();
    
    return true;
}

////////////////////////////////////////////////////////////
/// Clear the scene by removing all lights and hulls
////////////////////////////////////////////////////////////
void LightSystem::ClearScene() {
    for (td_LightSetIt it = m_Lights.begin(); it != m_Lights.end(); ++it) {
        delete (*it);
    }
    m_Lights.clear();
    for (td_HullSetIt it = m_Hulls.begin(); it != m_Hulls.end(); ++it) {
        delete (*it);
    }
    m_Hulls.clear();
}

////////////////////////////////////////////////////////////
/// Compute the lights in the view
////////////////////////////////////////////////////////////
void LightSystem::ComputeLights() {
    float fAlteredViewSize;
    unsigned uNumRenderedHulls;
    
    const float* pViewMatrix = m_pRenderWindow->getView().getTransform().getMatrix();

    PROFILER_START( "ComputeLights" )
    // Init the final texure
    m_FinalTexture.setActive();
    m_FinalTexture.clear( m_AmbientColor );

    glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
    PROFILER_CHECKPOINT( "ComputeLights", "Init" )
        
    // Render the static hulls on the static lights ONCE
    Light* pCurLight;
    for ( unsigned l = 0; l < m_StaticLightsToCompute.size(); ++l ) {
        pCurLight = m_StaticLightsToCompute[l];

        // Setup the view so it is centered on the current light
        fAlteredViewSize = pCurLight->GetRadius() * 2.0f;
        m_LightView.setCenter( pCurLight->GetCenter() );
        m_LightView.setSize( fAlteredViewSize, fAlteredViewSize);

        uNumRenderedHulls = RenderHullsOnLight( pCurLight, m_LightView.getTransform().getMatrix(), QuadTreeOccupant::eType_Static );
    
        // Render on the static texture
        ComputeLightShadow( pCurLight, uNumRenderedHulls );
        
        pCurLight->ComputeStatic( m_LightTexture, m_LightView.getTransform().getMatrix() );
    }
    m_StaticLightsToCompute.clear();
    PROFILER_CHECKPOINT( "ComputeLights", "Compute Static" )

    // For all visible light
    std::vector<QuadTreeOccupant*> visibleLights;
    m_LightsTree.QueryRegion( m_ViewAABB, visibleLights );

    const unsigned uNumVisibleLights = visibleLights.size();
    PROFILER_CHECKPOINT( "ComputeLights", "Query Lights" )
    for ( unsigned l = 0; l < uNumVisibleLights; ++l ) {
        pCurLight = static_cast<Light*>( visibleLights[l] );

        // Setup the view so it is centered on the current light
        fAlteredViewSize = pCurLight->GetRadius() * 2.0f;
        m_LightView.setCenter( pCurLight->GetCenter() );
        m_LightView.setSize( fAlteredViewSize, fAlteredViewSize );

        if ( pCurLight->IsStatic() ) {
            uNumRenderedHulls = RenderHullsOnLight( pCurLight, m_LightView.getTransform().getMatrix(), QuadTreeOccupant::eType_NonStatic );
        } else {
            uNumRenderedHulls = RenderHullsOnLight( pCurLight, m_LightView.getTransform().getMatrix() );
        }
        PROFILER_CHECKPOINT( "ComputeLights", "Render Hulls" )
    
        ComputeLightShadow( pCurLight, uNumRenderedHulls );
        
        // Draw on the final texture
        const sf::Vector2f& lowerBound( pCurLight->GetAABB().GetLowerBound() ),
            & upperBound( pCurLight->GetAABB().GetUpperBound() );

        m_FinalTexture.setActive();
        tools::SetupWindowProjection( 0, 0, m_FinalTexture.getSize().x, m_FinalTexture.getSize().y, pViewMatrix );

        if ( pCurLight->m_pStaticTexture ) {
            if ( uNumRenderedHulls > 0) {
                // Blend the static and dynamic parts of the light's
                m_BlendStaticDynamicShader.setParameter( "staticTexture", pCurLight->GetStaticTexture()->getTexture() );
                m_BlendStaticDynamicShader.setParameter( "dynamicTexture", m_LightTexture.getTexture() );

                tools::RenderShader( &m_BlendStaticDynamicShader, 
                                (int)lowerBound.x, (int)lowerBound.y,
                                (int)upperBound.x, (int)upperBound.y,
                                GL_SRC_ALPHA, GL_ONE );
            } else {
                pCurLight->RenderStatic();
            }
        } else {
            tools::RenderTexture( &m_LightTexture.getTexture(), 
                                (int)lowerBound.x, (int)lowerBound.y,
                                (int)upperBound.x, (int)upperBound.y,
                                GL_SRC_ALPHA, GL_ONE );
        }

        PROFILER_CHECKPOINT( "ComputeLights", "Render To Final" )
    }
    m_FinalTexture.setActive();
    m_FinalTexture.display();

    m_LightView.setCenter( 0.0f, 0.0f );
    m_LightView.setSize( (float)m_FinalTexture.getSize().x, (float)m_FinalTexture.getSize().y );

    sf::Vector2i halfSizeH( (int)(m_FinalTexture.getSize().x * 0.5f), (int)(m_FinalTexture.getSize().y * 0.5f) );

    /***************************** H-Blur *****************************/
    m_HBlurTex.setActive();
    m_HBlurTex.clear( sf::Color::Black );

    // Set the viewport
    tools::SetupWindowProjection( 0, 0, m_FinalTexture.getSize().x, m_FinalTexture.getSize().y, m_LightView.getTransform().getMatrix() );
    
    // Set the parameters
    m_BlurShader.setParameter( "texture", m_FinalTexture.getTexture() );
    m_BlurShader.setParameter( "off", 1.f / m_FinalTexture.getSize().x, 0.0f );
    m_BlurShader.setParameter( "blurRadius", m_fBlurRadius );

    // Render & display
    tools::RenderShader( &m_BlurShader, 
                    -halfSizeH.x, -halfSizeH.y, 
                    halfSizeH.x, halfSizeH.y, 
                    GL_ONE, GL_ZERO );

    m_HBlurTex.display();
    PROFILER_CHECKPOINT( "ComputeLights", "H-Blur" )

    /***************************** V-Blur *****************************/
    m_VBlurTex.setActive();
    m_VBlurTex.clear( sf::Color::Black );

    // Set the viewport
    tools::SetupWindowProjection( 0, 0, m_FinalTexture.getSize().x, m_FinalTexture.getSize().y, m_LightView.getTransform().getMatrix() );
    
    // Set the parameters
    m_BlurShader.setParameter( "texture", m_HBlurTex.getTexture() );
    m_BlurShader.setParameter( "off", 0.0f, 1.f / m_FinalTexture.getSize().y );
    m_BlurShader.setParameter( "blurRadius", m_fBlurRadius );
    
    // Render & display
    tools::RenderShader( &m_BlurShader, 
                    -halfSizeH.x, -halfSizeH.y, 
                    halfSizeH.x, halfSizeH.y, 
                    GL_ONE, GL_ZERO );

    m_VBlurTex.display();
    PROFILER_CHECKPOINT( "ComputeLights", "V-Blur" )

    m_pRenderWindow->resetGLStates();
    PROFILER_STOP( "ComputeLights" )
}

////////////////////////////////////////////////////////////
/// Render the lights
////////////////////////////////////////////////////////////
void LightSystem::RenderLights() {
    PROFILER_START( "RenderLights" )
    tools::SetupWindowProjection( m_Viewport.left, m_Viewport.top, m_Viewport.width, m_Viewport.height, m_pRenderWindow->getView().getTransform().getMatrix() );

    tools::RenderTexture( &m_VBlurTex.getTexture(), 
    //tools::RenderTexture( &m_FinalTexture.getTexture(), 
                    (int)m_ViewAABB.GetLowerBound().x, (int)m_ViewAABB.GetLowerBound().y,
                    (int)m_ViewAABB.GetUpperBound().x, (int)m_ViewAABB.GetUpperBound().y,
                    GL_DST_COLOR, GL_ZERO );

    m_pRenderWindow->resetGLStates();
    PROFILER_STOP( "RenderLights" )
}

////////////////////////////////////////////////////////////
/// Render the hulls for the given light
////////////////////////////////////////////////////////////
unsigned LightSystem::RenderHullsOnLight( Light* _pLight, const float* _pLightMatrix, QuadTreeOccupant::eType_Occupant _eHullsType ) {        
    // Init the light texture
    m_LightTexture.setActive();
    m_LightTexture.clear( sf::Color::Transparent );
    tools::SetupWindowProjection( 0, 0, TEXTURE_SIZE, TEXTURE_SIZE, _pLightMatrix );

    glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );

    // Get the hulls that the light affect
    std::vector<QuadTreeOccupant*> affectedHulls;
    Hull* pCurHull;
    m_HullsTree.QueryRegion( _pLight->GetAABB(), affectedHulls, _eHullsType );

    // Render them
    const unsigned uNumAffectedHulls = affectedHulls.size();
    for ( unsigned ch = 0; ch < uNumAffectedHulls; ++ch ) {
        pCurHull = static_cast<Hull*>( affectedHulls[ch] );
        if ( !pCurHull->IsPointInHull( _pLight->GetCenter() ) ) {
            pCurHull->Render( sf::Color::White );
        }
    }

    // Display the texture
    m_LightTexture.display();

    return uNumAffectedHulls;
}

////////////////////////////////////////////////////////////
/// Compute the light's shadow
////////////////////////////////////////////////////////////
void LightSystem::ComputeLightShadow( Light* _pLight, unsigned _uNumHulls ) {
    PROFILER_START( "RenderFinalTexture" )

    PROFILER_CHECKPOINT( "RenderFinalTexture", "Init" )
    m_ShadowMapTex.setActive();
    m_ShadowMapTex.clear( sf::Color::White );
    if ( _uNumHulls > 0) {
        /***************************** Compute the shadow map *****************************/
        m_AlternateView.setCenter( 0.0f, 0.0f );
        m_AlternateView.setSize( TEXTURE_SIZE, TEXTURE_SIZE );

        // Set the viewport
        m_AlternateView.setSize( TEXTURE_SIZE, 1 );
        //m_AlternateView.setSize( TEXTURE_SIZE, TEXTURE_SIZE );
        tools::SetupWindowProjection( 0, 0, TEXTURE_SIZE, 2, m_AlternateView.getTransform().getMatrix() );
        //tools::SetupWindowProjection( 0, 0, TEXTURE_SIZE, TEXTURE_SIZE, m_AlternateView.getTransform().getMatrix() );
    
        // Set the parameters
        m_ShadowMapShader.setParameter( "occludersMap", m_LightTexture.getTexture() );
        m_ShadowMapShader.setParameter( "textureSize", HALF_TEXTURE_SIZE );
        m_ShadowMapShader.setParameter( "heightRatio", 1.0f / HALF_TEXTURE_SIZE );
        m_ShadowMapShader.setParameter( "alphaThreshold", m_fAlphaThreshold );
        
        // Render & display
        tools::RenderShader( &m_ShadowMapShader, 
                            -HALF_TEXTURE_SIZE, -1, 
                            HALF_TEXTURE_SIZE, 1, 
                            //-HALF_TEXTURE_SIZE, -HALF_TEXTURE_SIZE, 
                            //HALF_TEXTURE_SIZE, HALF_TEXTURE_SIZE, 
                            GL_ONE, GL_ZERO );
    }
    m_ShadowMapTex.display();
    PROFILER_CHECKPOINT( "RenderFinalTexture", "Shadow Map" )

    /***************************** Draw Shadows *****************************/
    m_AlternateView.setCenter( 0.0f, 0.0f );
    m_AlternateView.setSize( TEXTURE_SIZE, TEXTURE_SIZE );

    m_DrawShadowsTex.setActive();
    m_DrawShadowsTex.clear( sf::Color::Black );

    // Set the viewport
    tools::SetupWindowProjection( 0, 0, TEXTURE_SIZE, TEXTURE_SIZE, m_AlternateView.getTransform().getMatrix() );
    
    // Set the parameters
    m_DrawShadowsShader.setParameter( "shadowMap", m_ShadowMapTex.getTexture() );
    m_DrawShadowsShader.setParameter( "lightColor", _pLight->GetColor().r / 255.0f, _pLight->GetColor().g / 255.0f, _pLight->GetColor().b / 255.0f, 1.0f );
    m_DrawShadowsShader.setParameter( "lightDirection", _pLight->GetDirection() );
    m_DrawShadowsShader.setParameter( "dotLit", _pLight->GetDotLit() );
    
    // Render & display
    tools::RenderShader( &m_DrawShadowsShader,
                        -HALF_TEXTURE_SIZE, -HALF_TEXTURE_SIZE, 
                        HALF_TEXTURE_SIZE, HALF_TEXTURE_SIZE, 
                        GL_ONE, GL_ZERO );

    m_DrawShadowsTex.display();
    PROFILER_CHECKPOINT( "RenderFinalTexture", "Draw Shadows" )

    /***************************** Final draw *****************************/
    m_LightTexture.setActive();
    tools::SetupWindowProjection( 0, 0, TEXTURE_SIZE, TEXTURE_SIZE, m_LightView.getTransform().getMatrix() );
    
    //sf::Texture::bind(&m_LightTexture.getTexture());
    //sf::Texture::bind(&m_ShadowMapTex.getTexture());
    //sf::Texture::bind(&m_DrawShadowsTex.getTexture());
    
    // Render
    const sf::Vector2f& lowerBound( _pLight->GetAABB().GetLowerBound() ),
        & upperBound( _pLight->GetAABB().GetUpperBound() );
    tools::RenderTexture( &m_DrawShadowsTex.getTexture(), 
                    (int)lowerBound.x, (int)lowerBound.y,
                    (int)upperBound.x, (int)upperBound.y,
                    GL_ONE, GL_ZERO );

    m_LightTexture.display();
    PROFILER_STOP( "RenderFinalTexture" )
}

////////////////////////////////////////////////////////////
/// Add a light, if the light is already in the system, don't add it
////////////////////////////////////////////////////////////
bool LightSystem::AddLight( Light* _pLight ) {
    if ( m_Lights.insert( _pLight ).second ) {
        m_LightsTree.Add( _pLight );
        SetStatic( _pLight, _pLight->IsStatic() );
        return true;
    }

    return false;
}

////////////////////////////////////////////////////////////
/// Remove a light
////////////////////////////////////////////////////////////
void LightSystem::RemoveLight( Light* _pLight, bool _bDeleteLight ) {
    td_LightSetIt it = m_Lights.find( _pLight );

    for ( td_LightVecIt it = m_StaticLightsToCompute.begin(); 
        it != m_StaticLightsToCompute.end();
        ++it ) {
            if ( _pLight == (*it) ) {
                m_StaticLightsToCompute.erase( it );
                break;
            }
    }

    if ( it != m_Lights.end() ) {
        m_Lights.erase( it );
    }

    if ( _bDeleteLight ) {
        delete _pLight;
    } else {
        _pLight->RemoveFromTree();
    }
}

////////////////////////////////////////////////////////////
/// Add a hull, if the hull is already in the system, don't add it
////////////////////////////////////////////////////////////
bool LightSystem::AddHull( Hull* _pHull ) {
    if ( m_Hulls.insert( _pHull ).second ) {
        m_HullsTree.Add( _pHull );
        SetStatic( _pHull, _pHull->IsStatic() );
        return true;
    }

    return false;
}

////////////////////////////////////////////////////////////
/// Remove a hull
////////////////////////////////////////////////////////////
void LightSystem::RemoveHull( Hull* _pHull, bool _bDeleteHull ) {
    td_HullSetIt it = m_Hulls.find( _pHull );

    if ( it != m_Hulls.end() ) {
        SetStatic( _pHull, _pHull->IsStatic() );
        m_Hulls.erase( it );
    }

    if ( _bDeleteHull ) {
        delete _pHull;
    } else {
        _pHull->RemoveFromTree();
    }
}

////////////////////////////////////////////////////////////
/// Set the staticity of an occupant
////////////////////////////////////////////////////////////
void LightSystem::SetStatic( QuadTreeOccupant* _pOccupant, bool _bIsStatic ) {
    Light* l = dynamic_cast< Light* >( _pOccupant );
    Hull* h = dynamic_cast< Hull* >( _pOccupant );
    
    if ( l ) {
        l->SetStatic( _bIsStatic );
        td_LightSetIt itLight = m_Lights.find( l );

        if ( _bIsStatic && itLight != m_Lights.end() ) {
            m_StaticLightsToCompute.push_back( l );
        }
    } else if ( h ) {
        h->SetStatic( _bIsStatic );
        td_HullSetIt itHull = m_Hulls.find( h );

        if ( itHull != m_Hulls.end() ) {
            std::vector<QuadTreeOccupant*> affectedLights;
            m_LightsTree.QueryRegion( h->GetAABB(), affectedLights, QuadTreeOccupant::eType_Static );
            for ( unsigned l = 0; l < affectedLights.size(); ++l ) {
                m_StaticLightsToCompute.push_back( static_cast< Light* >( affectedLights[l] ) );
            }
        }
    }
}

/*********************** Accessors ************************/
void LightSystem::SetView( const sf::View& _view ) {
    const sf::Vector2f lastViewSize( m_ViewAABB.GetSize() ),
        &newViewSize( _view.getSize() );
    m_Viewport = m_pRenderWindow->getViewport(_view);
    m_ViewAABB.Setup( _view.getCenter(), _view.getSize() );

    if (lastViewSize != newViewSize) {
        const sf::Vector2f& viewSize( m_ViewAABB.GetSize() );
        unsigned x = static_cast<unsigned>( viewSize.x ),
            y = static_cast<unsigned>( viewSize.y );

        // Final texture
        m_FinalTexture.create( x, y );
        m_FinalTexture.setSmooth( true );
        m_FinalTexture.setActive();
        glEnable( GL_BLEND );
        glEnable( GL_TEXTURE_2D );

        // Blur
        m_HBlurTex.create( x, y );
        m_VBlurTex.create( x, y );
    
        m_pRenderWindow->setActive();
    }
}

#ifdef _DEBUG
void LightSystem::DebugRender( bool _bShowQuadTrees, const sf::Color& _Color ) {
    tools::SetupWindowProjection( m_Viewport.left, m_Viewport.top, m_Viewport.width, m_Viewport.height, m_pRenderWindow->getView().getTransform().getMatrix() );

    if ( _bShowQuadTrees ) {
        m_LightsTree.DebugRender();
        m_HullsTree.DebugRender();
    }
    
    // For all visible light
    std::vector<QuadTreeOccupant*> visibleLights;
    m_LightsTree.QueryRegion( m_ViewAABB, visibleLights );
    unsigned uNumVisibleLights = visibleLights.size();
    Light* pCurLight;
    for ( unsigned l = 0; l < uNumVisibleLights; ++l ) {
        pCurLight = static_cast<Light*>( visibleLights[l] );
        pCurLight->DebugRender( _Color );
    }
    
    // For all visible hulls
    std::vector<QuadTreeOccupant*> visibleHulls;
    m_HullsTree.QueryRegion( m_ViewAABB, visibleHulls );
    unsigned uNumVisibleHulls = visibleHulls.size();
    Hull* pCurHull;
    for ( unsigned ch = 0; ch < uNumVisibleHulls; ++ch ) {
        pCurHull = static_cast<Hull*>( visibleHulls[ch] );
        pCurHull->DebugRender( _Color );
    }

    m_pRenderWindow->resetGLStates();
}
#endif //_DEBUG

    } // namespace light
} // namespace smeg